﻿//     Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;
using Gateway.Console.Models;
using Microsoft.Gateway.Utils;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Management;
using Microsoft.WindowsAzure.Management.Compute;
using Microsoft.WindowsAzure.Management.Compute.Models;
using Microsoft.WindowsAzure.Management.Models;
using Microsoft.WindowsAzure.Management.Storage;
using Microsoft.WindowsAzure.Management.Storage.Models;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;

namespace Gateway.Console.Services.Azure
{
    public class DeploymentManager : AzureServiceManagement, IDisposable
    {
        private Lazy<StorageManagementClient> _storageManagementClient;
        private Lazy<ComputeManagementClient> _computeManagementClient;
        private Lazy<ManagementClient> _subscriptionManagementClient;
        private readonly int _maxCopyRetries = 5;
        private readonly TimeSpan _copyRetrySleepPeriod = TimeSpan.FromMilliseconds(3000);
        private const string ThumbprintReplacement = "0000000000000000000000000000000000000000";
               
        public DeploymentManager(AuthenticationResult accessToken, string subscriptionId)
            : base(accessToken, subscriptionId)
        {
            _storageManagementClient = new Lazy<StorageManagementClient>(() => CloudContext.Clients.CreateStorageManagementClient(GetAzureCredentials(), AzureEnvironment.RdfeManagementEndpointUri));
            _computeManagementClient = new Lazy<ComputeManagementClient>(() => CloudContext.Clients.CreateComputeManagementClient(GetAzureCredentials(), AzureEnvironment.RdfeManagementEndpointUri));
            _subscriptionManagementClient = new Lazy<ManagementClient>(() => CloudContext.Clients.CreateManagementClient(GetAzureCredentials(), AzureEnvironment.RdfeManagementEndpointUri));
        }

        public DeploymentManager(AuthenticationResult accessToken, string subscrptionId, int maxCopyRetries, TimeSpan copyRetrySleepPeriod)
            : this(accessToken, subscrptionId)
        {
            _maxCopyRetries = maxCopyRetries;
            _copyRetrySleepPeriod = copyRetrySleepPeriod;
        }

        public new void Dispose()
        {
            if (_storageManagementClient.IsValueCreated)
            {
                _storageManagementClient.Value.Dispose();
            }
            if (_computeManagementClient.IsValueCreated)
            {
                _computeManagementClient.Value.Dispose();
            }
            if (_subscriptionManagementClient.IsValueCreated)
            {
                _subscriptionManagementClient.Value.Dispose();
            }
            base.Dispose();
        }

        public async Task<bool> DoesServiceAlreadyExistForSubscriptionAsync(string serviceName, string region)
        {
            try
            {
                var existingService = await _computeManagementClient.Value.HostedServices.GetAsync(serviceName);
                string serviceLocation = existingService.Properties.Location;
                if (!String.IsNullOrWhiteSpace(existingService.Properties.AffinityGroup))
                {
                    var affinityGroup = await _subscriptionManagementClient.Value.AffinityGroups.GetAsync(existingService.Properties.AffinityGroup);
                    serviceLocation = affinityGroup.Location;
                }
                return String.Equals(serviceLocation, region, StringComparison.OrdinalIgnoreCase);
            }
            catch
            {
                return false;
            }
        }

        public async Task<string> GetServiceVIPAddressAsync(string serviceName)
        {
            try
            {
                var deployment = await _computeManagementClient.Value.Deployments.GetBySlotAsync(serviceName, DeploymentSlot.Production);
                return deployment.VirtualIPAddresses.First().Address;
            }
            catch
            {
            }
            return String.Empty;
        }

        public async Task<Tuple<bool, string>> CheckServiceNameAvailableAsync(string serviceName)
        {
            var response = await _computeManagementClient.Value.HostedServices.CheckNameAvailabilityAsync(serviceName);
            return Tuple.Create(response.IsAvailable, response.Reason);
        }

        public async Task<bool> DeployCloudServiceAsync(Deployment deployment)
        {
            var success = true;            

            try
            {
                bool deployNewService = true;
                if (await DoesServiceAlreadyExistForSubscriptionAsync(deployment.ServiceName, deployment.RegionSelection))
                {
                    deployNewService = false;
                }
                else
                { 
                    var isAvailable = await CheckServiceNameAvailableAsync(deployment.ServiceName);
                    if (isAvailable.Item1)
                    {
                        //Create storage account if one was requested
                        if (!string.IsNullOrWhiteSpace(deployment.CreateStorageAccountName))
                        {
                            await UpdateDeploymentProgressAsync(deployment, new DeployStatus(string.Format("Creating Storage Account '{0}'", deployment.CreateStorageAccountName)));

                            var saStatus = await CreateStorageAccountAsync(deployment.CreateStorageAccountName, deployment.RegionSelection);
                            deployment.SelectedStorageAccount = deployment.CreateStorageAccountName;

                            await UpdateDeploymentProgressAsync(deployment, saStatus);
                            success &= saStatus.Success;
                        }
                    }
                    else
                    {
                        success = false;
                        await UpdateDeploymentProgressAsync(deployment, new DeployStatus()
                        {
                            Message = string.Format("Service Name {0}.{1} is not available", deployment.ServiceName, AzureEnvironment.AzureCloudServiceUriSuffix),
                            Success = success
                        });
                    }
                }
                if (success)
                {
                    deployment.StorageAccountKey = await GetStorageAccountSecondaryKeyAsync(deployment.SelectedStorageAccount);
                    await UpdateDeploymentProgressAsync(deployment,
                        new DeployStatus(string.Format("{0} cloud service slot '{1}'", deployNewService ? "Creating" : "Updating", deployment.ServiceName)));
                    if (deployNewService)
                    {
                        //Create cloud service
                        var createCSStatus = await CreateCloudServiceAsync(deployment.ServiceName, deployment.RegionSelection);
                        success &= createCSStatus.Success;
                        await UpdateDeploymentProgressAsync(deployment, createCSStatus);

                        if (success & deployment.IsHttpsEnabled)
                        {
                            var uploadCertStatus = await UploadCertificateAsync(deployment.ServiceName, deployment.CertificateContents, deployment.CertificatePassword);
                            success &= uploadCertStatus.Success;
                            await UpdateDeploymentProgressAsync(deployment, uploadCertStatus);
                        }
                    }
                    else
                    {
                        // Update the cloud service to ensure ReverseDNS is enabled
                        var updateCSStatus = await UpdateCloudServiceAsync(deployment.ServiceName);
                        success &= updateCSStatus.Success;
                        await UpdateDeploymentProgressAsync(deployment, updateCSStatus);
                    }
                }

                if (success)
                {
                    await UpdateDeploymentProgressAsync(deployment, new DeployStatus(string.Format("Configuring cloud service deployment '{0}'", deployment.ServiceName)));

                    //deploy the cloud service
                    var deployStatus = await ProvisionCloudServiceAsync(deployment);
                    success &= deployStatus.Success;
                    await UpdateDeploymentProgressAsync(deployment, deployStatus);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
                success = false;

                UpdateDeploymentProgress(deployment, new DeployStatus()
                {
                    Message = string.Format("Error creating service. Error {0}", ex.Message),
                    Success = success
                });
            }

            return success;
        }        

        public static void UpdateDeploymentProgress(Deployment deployment, DeployStatus status)
        {
            UpdateDeploymentProgressAsync(deployment, status).Wait();
        }

        public static async Task UpdateDeploymentProgressAsync(Deployment deployment, DeployStatus status)
        {
            status.DeploymentId = deployment.DeploymentId;            

            using(var db = new ConsoleContext())
            {                
                db.DeploymentStatus.Add(status);
                await db.SaveChangesAsync();
            }
        } 

        public async Task<List<string>> GetStorageAccountsAsync()
        {
            var storageAccounts = await _storageManagementClient.Value.StorageAccounts.ListAsync();
            return storageAccounts.Select(sa => sa.Name).ToList();
        }

        private async Task<DeployStatus> CreateCloudServiceAsync(string serviceName, string region)
        {
            var status = new DeployStatus();
            status.Message = string.Format("Created Cloud Service slot '{0}' successfully", serviceName);

            try
            {
                //Create the hosted service
                var response = await _computeManagementClient.Value.HostedServices.CreateAsync(new HostedServiceCreateParameters
                    {
                        Location = region,
                        ServiceName = serviceName,
                        ReverseDnsFqdn = String.Format("{0}.{1}.", serviceName, AzureEnvironment.AzureCloudServiceUriSuffix),
                    });

                status.Success = response.StatusCode == System.Net.HttpStatusCode.Created;
                if(!status.Success)
                {
                    status.Message = string.Format("Error Creating Cloud Service slot '{0}', status code {1}", serviceName, response.StatusCode);
                }
            }
            catch(Exception ex)
            {
                Debug.WriteLine(ex.ToString());
                status.Success = false;
                status.Message = string.Format("Error Creating Cloud Service slot '{0}', Error {1}", serviceName, ex.Message);
            }           
                  
            return status;
        }

        private async Task<DeployStatus> UpdateCloudServiceAsync(string serviceName)
        {
            var status = new DeployStatus
            {
                Message = String.Format("Updated Cloud Service slot '{0}' successfully", serviceName),
            };

            try
            {
                //Create the hosted service
                var response = await _computeManagementClient.Value.HostedServices.UpdateAsync(serviceName, new HostedServiceUpdateParameters
                    {
                        ReverseDnsFqdn = String.Format("{0}.{1}.", serviceName, AzureEnvironment.AzureCloudServiceUriSuffix),
                    });

                status.Success = response.StatusCode == System.Net.HttpStatusCode.OK;
                if (!status.Success)
                {
                    status.Message = string.Format("Error updating Cloud Service slot '{0}', status code {1}", serviceName, response.StatusCode);
                }
            }
            catch (Exception ex)
            {
                status.Success = false;
                status.Message = string.Format("Error updating Cloud Service slot '{0}', Error {1}", serviceName, ex.Message);
            }

            return status;
        }

        private async Task<DeployStatus> UploadCertificateAsync(string serviceName, byte[] rawCert, string certPassword = null)
        {            
           var status = new DeployStatus();
           status.Message = string.Format("Certificate uploaded for cloud service '{0}' successfully", serviceName);

           try
           {
                var response = await _computeManagementClient.Value.ServiceCertificates.CreateAsync(serviceName, new ServiceCertificateCreateParameters() 
                {
                    CertificateFormat = CertificateFormat.Pfx, 
                    Data = rawCert, 
                    Password = certPassword.ToString()
                });               
               
                status.Success = response.StatusCode == System.Net.HttpStatusCode.OK;
                if (!status.Success)
                {
                    status.Message = string.Format("Error uploading certificate for cloud service '{0}', status code {1}", serviceName, response.StatusCode);
                }
           }
           catch(Exception ex)
           {
               status.Success = false;
               status.Message = string.Format("Error uploading certificate for hosted service '{0}', Error {1}", serviceName, ex.Message);
           }

           return status;
        }

        private async Task<DeployStatus> ProvisionCloudServiceAsync(Deployment deployment)
        {
            var status = new DeployStatus();
            status.Success = true;
 
            using (var config = await _computeManagementClient.Value.HttpClient.GetAsync(deployment.ServiceConfigTemplateLocation))
            {
                if (config.IsSuccessStatusCode)
                {                        
                    using (var configStream = await config.Content.ReadAsStreamAsync())
                    {                         
                        //Update the service configuration *.cscfg for the specifics for this deployment                             
                        var document = XDocument.Load(configStream);
                        var svcConfig = document.Root;
                        var ns = (XNamespace)svcConfig.Attribute("xmlns").Value;
                        var instances = svcConfig.Element(ns + "Role").Element(ns + "Instances");
                        var settings = svcConfig.Element(ns + "Role").Elements(ns + "ConfigurationSettings");                            
                        var configLocation = settings.Descendants().First(x => x.Attribute("name").Value == "ConfigLocation");                            
                        var diagnosticsConnection = settings.Descendants().First(x => x.Attribute("name").Value == "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString");
                        var azureEnvironment = settings.Descendants().FirstOrDefault(x => x.Attribute("name").Value == "AzureEnvironment");

                        instances.SetAttributeValue("count", deployment.InstanceCount);
                        configLocation.SetAttributeValue("value", deployment.RoleIndexConfigLocation);
                        if (azureEnvironment != null)
                        {
                            azureEnvironment.SetAttributeValue("value", Configuration.AzureEnvironment.ToString());
                        }
                        var storageAccountConnection = GetStorageAccountConnectionString(deployment.SelectedStorageAccount, deployment.StorageAccountKey);
                        diagnosticsConnection.SetAttributeValue("value", storageAccountConnection);
                            
                        //upload to storage account                            
                        var package = await CopyDeploymentPackageAsync(deployment, CloudStorageAccount.Parse(storageAccountConnection), "deployments", _maxCopyRetries, _copyRetrySleepPeriod);
                        if (package == null)
                        {
                            status.Success = false;
                            status.Message = "Failed to copy requested deployment package to blob container";
                        }
                        else
                        {
                            if (deployment.IsHttpsEnabled)
                            {
                                var certificates = svcConfig.Element(ns + "Role").Elements(ns + "Certificates");
                                var thumprintSetting = certificates.Descendants().First(x => x.Attribute("name").Value == "CertificateName");
                                thumprintSetting.SetAttributeValue("thumbprint", deployment.CertificateThumbprint);
                            }
                            try
                            {
                                await UpdateDeploymentProgressAsync(deployment, new DeployStatus(string.Format("Deploying cloud service '{0}'", deployment.ServiceName)));
                                //deploy the cloud service into the provisioned slot using the uploaded *.cspkg and *.cscfg
                                Microsoft.WindowsAzure.OperationStatusResponse response;
                                if ((await GetServiceDeploymentSlot(deployment.ServiceName, DeploymentSlot.Production)) == null)
                                {
                                    response = await _computeManagementClient.Value.Deployments.CreateAsync(deployment.ServiceName,
                                        DeploymentSlot.Production,
                                        new DeploymentCreateParameters
                                        {
                                            Label = deployment.ServiceName,
                                            Name = deployment.ServiceName,
                                            PackageUri = package.Uri,
                                            Configuration = document.ToString(),
                                            StartDeployment = true,
                                        });
                                }
                                else
                                {
                                    response = await _computeManagementClient.Value.Deployments.UpgradeBySlotAsync(deployment.ServiceName,
                                        DeploymentSlot.Production,
                                        new DeploymentUpgradeParameters
                                        {
                                            Label = deployment.ServiceName,
                                            PackageUri = package.Uri,
                                            Configuration = document.ToString(),
                                            Mode = DeploymentUpgradeMode.Auto,
                                        });
                                }

                                status.Success = response.StatusCode == HttpStatusCode.OK && response.Error == null;
                                if (!status.Success)
                                {
                                    status.Message = String.Format("Error Deploying cloud service '{0}', \r\n Response code {1} \r\n Error code: {2} \r\n Error message: {3} ",
                                        deployment.ServiceName, response.StatusCode, response.Error.Code, response.Error.Message);
                                }
                            }
                            catch (Exception ex)
                            {
                                Debug.WriteLine(ex.ToString());
                                status.Success = false;
                                status.Message = String.Format("Error Deploying cloud service '{0}', \r\n Error message: {1}", deployment.ServiceName, ex.Message);
                            }
                        }
                    }
                }
                else
                {
                    status.Success = false;
                    status.Message = string.Format("Unable to locate service configuration template at {0}, On requesting template response status code is {1}", 
                        deployment.ServiceConfigTemplateLocation, config.StatusCode);              
                }
            }

            if (status.Success)
            {
                status.Message = string.Format("Finished Deployment of cloud service '{0}' successfully", deployment.ServiceName);
            }

            return status;
        }

        public async Task<LocationsListResponse> GetSubscriptionRegionsAsync()
        {
            return await _subscriptionManagementClient.Value.Locations.ListAsync();
        }

        private async Task<DeployStatus> CreateStorageAccountAsync(string storageAccountName, string region)
        {
            var status = new DeployStatus();

            //Create a storage account in the given region
            var response = await _storageManagementClient.Value.StorageAccounts.CreateAsync(
                new StorageAccountCreateParameters
                {
                    Name = storageAccountName,
                    Location = region,
                });

            status.Success = response.StatusCode == HttpStatusCode.OK;
            status.Message = (status.Success) ? string.Format("Storage Account '{0}' created successfully", storageAccountName) : string.Format("{0} {1}", response.Error.Code, response.Error.Message);

            return status;
        }        

        private async Task<string> GetStorageAccountSecondaryKeyAsync(string storageAccountName)
        {
            //Retrieve the storage account keys
            var keys = await _storageManagementClient.Value.StorageAccounts.GetKeysAsync(storageAccountName);

            return keys.SecondaryKey;
        }

        private async Task<CloudBlockBlob> CopyDeploymentPackageAsync(Deployment deployment, CloudStorageAccount destStorageAccount, string destContainerName, int maxRetries, TimeSpan sleepPeriod)
        {
            var sourceServicePackageUri = deployment.ServicePackageFileLocation;
            var blobName = Path.GetFileName(new Uri(deployment.ServicePackageFileLocation).AbsolutePath);

            //upload cloud service package and config to storage account            
            var blobs = destStorageAccount.CreateCloudBlobClient();
            var destContainer = blobs.GetContainerReference(destContainerName);
            await destContainer.CreateIfNotExistsAsync();
            await destContainer.SetPermissionsAsync(
                new BlobContainerPermissions()
                {
                    PublicAccess = BlobContainerPublicAccessType.Blob
                });
            
            var blob = destContainer.GetBlockBlobReference(blobName);
            //copy from source release storage account to gateway console owners storage account without 
            // round tripping the *.cspkg to the client
            blob.StartCopyFromBlob(new Uri(sourceServicePackageUri));
            var copySuccess = await WaitOnCopyAsync(sourceServicePackageUri, blobName, destContainer, _maxCopyRetries, _copyRetrySleepPeriod);
            if (!copySuccess)
            {
                return null;
            }
            return blob;
        }

        private static async Task<bool> WaitOnCopyAsync(string sourceServicePackageUri, string blobName, CloudBlobContainer destContainer, int maxRetries, TimeSpan sleepPeriod)
        {
            bool pendingCopy = true;
          
            while (pendingCopy && maxRetries > 0)
            {
                maxRetries--;

                //update CopyStatus 
                var blob = await destContainer.GetBlobReferenceFromServerAsync(blobName);

                if (blob.CopyState.Status != CopyStatus.Success && blob.CopyState.Status != CopyStatus.Pending)
                {
                    blob.StartCopyFromBlob(new Uri(sourceServicePackageUri));
                }

                pendingCopy = blob.CopyState.Status != CopyStatus.Success;

                if (pendingCopy)
                {
                    Thread.Sleep((int)sleepPeriod.TotalMilliseconds);
                }
            }

            return !pendingCopy;
        }

        public void DeleteDeployment(string serviceName)
        {
            try
            {                
                _computeManagementClient.Value.Deployments.DeleteBySlot(serviceName, DeploymentSlot.Production);
                _computeManagementClient.Value.HostedServices.Delete(serviceName);
            }
            catch
            {
             // deployment was deleted already in the Portal
            }
        }

        private string GetStorageAccountConnectionString(string accountName, string accountKey)
        {
            return string.Format(
                        CultureInfo.InvariantCulture,
                        "DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};BlobEndpoint={2};QueueEndpoint={3};TableEndpoint={4};",
                        accountName, 
                        accountKey,
                        AzureEnvironment.StorageAccountEndpoint(accountName, "blob"),
                        AzureEnvironment.StorageAccountEndpoint(accountName, "queue"),
                        AzureEnvironment.StorageAccountEndpoint(accountName, "table"));
        }

        private async Task<DeploymentGetResponse> GetServiceDeploymentSlot(string serviceName, DeploymentSlot slot)
        {
            try
            {
                return await _computeManagementClient.Value.Deployments.GetBySlotAsync(serviceName, slot);
            }
            catch
            {
                return null;
            }
        }
    }
}


